import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt
import math

# TODO Create GUI interface for the application

example_array = np.array([545, 1187, 2826, 3884, 4469, 4166, 3646, 2770,
                          1926, 1776, 3383, 229, 1743, 3888, 2731, 2084, 4671,
                          6777, 10295, 6703, 1522, 3581, 4249, 4667, 5748,
                          4249, 3840, 3296, 8199, 6761, 1737, 3049, 577, 1368,
                          5934, 4078, 2719, 2006, 2956, 8823, 3386, 2258,
                          5682, 2873, 3068, 3116, 3501, 1291, 1435, 899, 3472,
                          1736, 1207, 4918, 3359, 2050, 2244, 2626, 7910,
                          3575, 11190, 1115, 2878, 3256, 13062, 6764, 8075,
                          6565, 3000, 6561, 3168, 6885, 6401, 2006, 4038,
                          5899, 4708, 4993, 2399, 4392, 3698, 5141, 3127,
                          6162, 6543, 1609, 3265, 1306, 6640, 3216, 10444,
                          6282, 5806, 6818, 8527, 5791, 8712, 1398, 3936,
                          1569, 1725, 2794, 462, 1564, 4760, 3245, 585, 924,
                          1281, 1780, 2839, 2700, 7376, 10588, 4408, 6244,
                          5015, 9611, 1899, 5060, 6420, 2864, 5925, 7101,
                          7138, 788, 5534, 3792, 3593, 4302, 1602, 5300,
                          6708, 5669, 2250, 5844, 3031, 2408, 5412, 1520,
                          3694, 2980, 2724, 3910, 4593, 2675, 1921, 2800,
                          4432, 2611, 1110, 3631, 1933, 1861, 1762, 425, 1491,
                          728, 3035, 1800, 1323, 3517, 659, 3833, 3578, 997,
                          3953, 3321, 5081, 5961, 2344, 9773, 6785, 2614,
                          4883, 5577, 3720, 3715, 6156, 5117, 6717, 4341,
                          4296, 3279, 4330, 4393, 4538, 2799, 4704, 3134, 3371,
                          323, 1670, 5156, 3186, 2670, 2349, 4374, 2147, 2081,
                          1830, 3047, 2164, 2538, 947, 1764, 248, 884, 1912,
                          935, 5195, 1600, 921, 676, 2164, 2554, 3416, 2656,
                          3567, 5666, 2700, 6423, 3505, 3143, 7734, 5626,
                          11257, 4822, 4311, 4080, 5528, 6441, 6450, 3800,
                          2569, 5216, 5175, 3558, 3435, 3067, 4722, 1506,
                          1822, 5016, 383, 519, 2371, 7269, 2686, 858, 4106,
                          2535, 1951, 6816, 1819, 2941, 791, 792, 939, 2772,
                          1201, 126, 2227, 3871, 766, 329, 1415, 1451, 3883,
                          1134, 4397, 5723, 3522, 7975, 6775, 4267, 8261,
                          4067, 5624, 4881, 2008, 5316, 4314, 7457, 6738,
                          4640, 8356, 2511, 3620, 1981, 1839, 5789, 1855,
                          3981, 2906, 2333, 1287, 3727, 17587, 9121, 8376,
                          1562, 2544, 3916, 2644, 737, 3781, 1703, 637, 1058,
                          1092, 2420, 784, 2459, 3, 464, 557, 1123, 66, 2933,
                          1955, 2302, 3590, 2130, 3169, 3946, 5105, 3067,
                          3341, 4716, 3603, 3179, 2054, 2910, 4635, 3983,
                          3638, 7010, 8729, 4035, 4654, 5308, 3309.99, 2800,
                          4375, 3019, 1849, 4667, 5661, 5666, 3370, 3257,
                          2870, 2540, 2379, 1460, 3121, 1276, 2742, 223,
                          1680, 2403, 1743, 1365, 3908, 2147, 1865, 2615,
                          3466, 1788, 3155, 1998, 3909, 6700, 6301, 3488,
                          3073, 5568, 3685, 3722, 10468, 7154], dtype=float)


class RegFits(object):
    '''
    Applies regression anaylsis to an array of data sumbitted upon
    class creation.  Finds the slope of polynomial regressions from 2nd
    to 8th degree.  Finds linear regression.  Also applies the Akaike
    Information Criterion test to find the slope that introduces the least
    amount of entropy.
    '''
    def __init__(self, Y):
        self.Y = Y
        self.X = np.array([i for i in range(0, len(self.Y))], dtype=float)
        self.poly8g = 0
        self.poly7g = 0
        self.poly6g = 0
        self.poly5g = 0
        self.poly4g = 0
        self.poly3g = 0
        self.poly2g = 0
        self.lng = 0
        self.rss = {'poly8': 0, 'poly7': 0, 'poly6': 0, 'poly5': 0,
                    'poly4': 0, 'poly3': 0, 'poly2': 0, 'ln': 0}
        self.aic_results = {'poly8': 0, 'poly7': 0, 'poly6': 0, 'poly5': 0,
                            'poly4': 0, 'poly3': 0, 'poly2': 0, 'ln': 0}
        self.graph_point = 0
        self.best_ft_name = 0

    def poly8(self):
        '''
        8th Degree Polynomial Regression
        '''
        Z = np.polyfit(self.X, self.Y, 8)
        self.poly8g = np.array([((Z[0] * w ** 8) + (Z[1] * w ** 7)
                                 + (Z[2] * w ** 6) + (Z[3] * w ** 5)
                                 + (Z[4] * w ** 4) + (Z[5] * w ** 3)
                                 + (Z[6] * w ** 2) + (Z[7] * w) + Z[8])
                                for w in range(0, len(self.Y))], dtype=float)

        P = np.array([(self.Y - self.poly8g) ** 2])
        self.rss['poly8'] = P.sum()
        return

    def poly7(self):
        '''
        7th Degree Polynomial Regression
        '''
        Z = np.polyfit(self.X, self.Y, 7)
        self.poly7g = np.array([((Z[0] * w ** 7) + (Z[1] * w ** 6)
                                 + (Z[2] * w ** 5) + (Z[3] * w ** 4)
                                 + (Z[4] * w ** 3) + (Z[5] * w ** 2)
                                 + (Z[6] * w) + Z[7])
                                for w in range(0, len(self.Y))], dtype=float)

        P = np.array([(self.Y - self.poly7g) ** 2])
        self.rss['poly7'] = P.sum()
        return

    def poly6(self):
        '''
        6th Degree Polynomial Regression
        '''
        Z = np.polyfit(self.X, self.Y, 6)
        self.poly6g = np.array([((Z[0] * w ** 6) + (Z[1] * w ** 5)
                                 + (Z[2] * w ** 4) + (Z[3] * w ** 3)
                                 + (Z[4] * w ** 2) + (Z[5] * w) + Z[6])
                                for w in range(0, len(self.Y))], dtype=float)

        P = np.array([(self.Y - self.poly6g) ** 2])
        self.rss['poly6'] = P.sum()
        return

    def poly5(self):
        '''
        5nd Degree Polynomial Regression
        '''
        Z = np.polyfit(self.X, self.Y, 5)
        self.poly5g = np.array([((Z[0] * w ** 5) + (Z[1] * w ** 4)
                                 + (Z[2] * w ** 3) + (Z[3] * w ** 2)
                                 + (Z[4] * w) + Z[5])
                                for w in range(0, len(self.Y))], dtype=float)

        P = np.array([(self.Y - self.poly5g) ** 2])
        self.rss['poly5'] = P.sum()
        return

    def poly4(self):
        '''
        4nd Degree Polynomial Regression
        '''
        Z = np.polyfit(self.X, self.Y, 4)
        self.poly4g = np.array([((Z[0] * w ** 4) + (Z[1] * w ** 3)
                                 + (Z[2] * w ** 2) + (Z[3] * w) + Z[4])
                                for w in range(0, len(self.Y))], dtype=float)

        P = np.array([(self.Y - self.poly4g) ** 2])
        self.rss['poly4'] = P.sum()
        return

    def poly3(self):
        '''
        3nd Degree Polynomial Regression
        '''
        Z = np.polyfit(self.X, self.Y, 3)
        self.poly3g = np.array([((Z[0] * w ** 3) + (Z[1] * w ** 2)
                                 + (Z[2] * w) + Z[3])
                                for w in range(0, len(self.Y))], dtype=float)

        P = np.array([(self.Y - self.poly3g) ** 2])
        self.rss['poly3'] = P.sum()
        return

    def poly2(self):
        '''
        2nd Degree Polynomial Regression
        '''
        Z = np.polyfit(self.X, self.Y, 2)
        self.poly2g = np.array([((Z[0] * w ** 2) + (Z[1] * w) + Z[2])
                                for w in range(0, len(self.Y))], dtype=float)

        P = np.array([(self.Y - self.poly2g) ** 2])
        self.rss['poly2'] = P.sum()
        return

    def linr(self):
        '''
        Linear Regression
        '''
        Z = stats.linregress(self.X, self.Y)
        self.lng = np.array([((Z[0] * w) + Z[1])
                      for w in range(0, len(self.Y))], dtype=float)

        P = np.array([(self.Y - self.lng) ** 2])
        self.rss['ln'] = P.sum()
        return

    def aic(self, rss, k, n):
        '''
        Akaike Information Criteria

        rss - Residual Sum of Square
        k - number of parameters
        n - number of observations
        '''
        return (n * math.log(rss / n)) + (2 * k)

    def aicc(self, rss, k, n):
        '''
        Corrected Akaike Information Criteria
        This should be used in almost all cases.

        rss - Residual Sum of Square
        k - number of parameters
        n - number of observations
        '''
        aic_score = self.aic(rss, k, n)
        return aic_score + (2 * k * (k + 1)) / (n - k - 1)

    def best_fit(self):
        '''
        Entry method to find line of best fit.
        Returns name of line of best fit, and array of values created
        by the slope of best fit, using the Corrected Akaike Information
        Criteria.
        '''
        self.poly8()
        self.poly7()
        self.poly6()
        self.poly5()
        self.poly4()
        self.poly3()
        self.poly2()
        self.linr()
        self.aic_results['poly8'] = self.aicc(self.rss['poly8'], 8, len(self.X))
        self.aic_results['poly7'] = self.aicc(self.rss['poly7'], 7, len(self.X))
        self.aic_results['poly6'] = self.aicc(self.rss['poly6'], 6, len(self.X))
        self.aic_results['poly5'] = self.aicc(self.rss['poly5'], 5, len(self.X))
        self.aic_results['poly4'] = self.aicc(self.rss['poly4'], 4, len(self.X))
        self.aic_results['poly3'] = self.aicc(self.rss['poly3'], 3, len(self.X))
        self.aic_results['poly2'] = self.aicc(self.rss['poly2'], 2, len(self.X))
        self.aic_results['ln'] = self.aicc(self.rss['ln'], 1, len(self.X))

        for key, value in self.aic_results.iteritems():
            if value == min([self.aic_results['poly8'],
                             self.aic_results['poly7'],
                             self.aic_results['poly6'],
                             self.aic_results['poly5'],
                             self.aic_results['poly4'],
                             self.aic_results['poly3'],
                             self.aic_results['poly2'],
                             self.aic_results['ln']]):
                self.best_ft_name = key

        self.graph_point = {'poly8': self.poly8g, 'poly7': self.poly7g,
                    'poly6': self.poly6g, 'poly5': self.poly5g,
                    'poly4': self.poly4g, 'poly3': self.poly3g,
                    'poly2': self.poly2g, 'ln': self.lng}

        return self.best_ft_name, self.graph_point[self.best_ft_name]


def moving_avg(X, k=3, t=False):
    '''
    Returns an array with moving averages of given array with a centered
    lag of k.

    X - array of data.
    k - size of convolve window; default 3
    t - Boolean to LN transform, default False
    '''
    if t == False:
        pass
    else:
        X = np.log(X)
    weightings = np.repeat(1.0, k) / k
    return np.convolve(X, weightings)[k - 1: - (k - 1)]


c = RegFits(example_array)
c.best_fit()

d = RegFits(moving_avg(example_array, k=12))
d.best_fit()

plt.subplot(211)
plt.plot(d.X, d.Y, color='grey')
plt.plot(d.X, d.graph_point[d.best_ft_name], color='red', lw=2)
plt.title(d.best_ft_name)
plt.subplot(212)
plt.plot(c.X, c.Y, color='grey')
plt.plot(c.X, c.graph_point[c.best_ft_name], color='red', lw=2)
plt.title(c.best_ft_name)

plt.show()
